// Copyright (c) 2020 Presto Labs Pte. Ltd.
// Author: leon

#pragma once

#include <map>
#include <string>
#include <vector>

#include "coin2/exchange/base/executor_util/market_ts.h"
#include "coin2/exchange/base/feed/processor.h"
#include "coin2/exchange/huobi/symbology/product.h"
#include "coin2/exchange/feed.pb.h"

namespace coin2::exchange::huobi::feed_v1 {
namespace impl {

using coin2::exchange::feed::WebsocketConfig;

class HuobiRequestGenerator : public base::feed::RequestGenerator {
 public:
  explicit HuobiRequestGenerator(const WebsocketConfig* ws_config)
      : config_(ws_config) {}

  std::vector<std::string> GenerateRequests(
    const std::vector<std::string>& channels,
    const std::vector<std::string>& symbols,
    const std::map<std::string, std::string>& channel_templates,
    const std::string& request_template) override {
    std::vector<std::string> reqs;
    std::map<std::string, std::string> request_channel_templates;
    std::vector<std::string> request_channels;
    std::string request_template_adjust;
    if (request_template.find("{unsub}") != std::string::npos) {
      // unsubscription_req = '{{unsub}}'
      request_template_adjust = "{{channel}}";
      GetUnsubscribeTemplate(channels, channel_templates,
          &request_channels, &request_channel_templates);
    } else {
      request_template_adjust = request_template;
      request_channels = channels;
      request_channel_templates = channel_templates;
    }

    std::vector<std::string> ch = request_channels;
    auto it = std::find(ch.begin(), ch.end(), "nav");
    if (it != ch.end()) {
      auto timestamp = MarketTimestampProvider::GetTimestamp();
      for (auto& symbol : symbols) {
        auto product = symbology::HuobiProduct::FromNativeStr(symbol, timestamp);
        for (auto etp_type : {"3L", "3S", "2L", "2S", "1S"}) {
          if (boost::algorithm::ends_with(product.base().currency(), etp_type)) {
            std::string c = request_channel_templates.at("nav");
            boost::replace_all(c, "{}", symbol);
            std::string s = request_template_adjust;
            boost::replace_all(s, "{channel}", c);
            reqs.push_back(s);
          }
        }
      }
      ch.erase(it);
    }
    it = std::find(ch.begin(), ch.end(), "req_snapshot");
    if (it != ch.end()) {
      for (auto& symbol : symbols) {
        std::string c = request_channel_templates.at("req_snapshot");
        if (symbol == "wemixusdt") {
          boost::replace_all(c, "mbp.5", "mbp.150");
        }
        boost::replace_all(c, "{}", symbol);
        std::string s = request_template_adjust;
        boost::replace_all(s, "{channel}", c);
        reqs.push_back(s);
      }
      ch.erase(it);
    }
    it = std::find(ch.begin(), ch.end(), "mbp5");
    if (it != ch.end()) {
      for (auto& symbol : symbols) {
        std::string c = request_channel_templates.at("mbp5");
        if (symbol == "wemixusdt") {
          boost::replace_all(c, "mbp.5", "mbp.150");
        }
        boost::replace_all(c, "{}", symbol);
        std::string s = request_template_adjust;
        boost::replace_all(s, "{channel}", c);
        reqs.push_back(s);
      }
      ch.erase(it);
    }

    std::vector<std::string> rest_channels;
    auto timestamp = MarketTimestampProvider::GetTimestamp();
    BuildKlineRequest(ch, symbols, request_channel_templates, request_template_adjust,
        timestamp, config_, &last_long_query_time_, &rest_channels, &reqs);
    ch = rest_channels;

    std::vector<std::string> reqs_others = RequestGenerator::GenerateRequests(
        ch, symbols, request_channel_templates, request_template_adjust);
    reqs.insert(reqs.end(), reqs_others.begin(), reqs_others.end());

    return reqs;
  }

  static int64_t GetPeriod(const std::string& channel) {
    if (channel.find("period=1min") != std::string::npos) {
      return (int64_t)60 * 1'000'000'000;
    } else if (channel.find("period=5min") != std::string::npos) {
      return (int64_t)5 * 60 * 1'000'000'000;
    } else if (channel.find("period=15min") != std::string::npos) {
      return (int64_t)15 * 60 * 1'000'000'000;
    } else if (channel.find("period=30min") != std::string::npos) {
      return (int64_t)30 * 60 * 1'000'000'000;
    } else if (channel.find("period=60min") != std::string::npos) {
      return (int64_t)60 * 60 * 1'000'000'000;
    } else if (channel.find("period=4hour") != std::string::npos) {
      return (int64_t)4 * 60 * 60 * 1'000'000'000;
    } else if (channel.find("period=1day") != std::string::npos) {
      return (int64_t)24 * 60 * 60 * 1'000'000'000;
    } else if (channel.find("period=1mon") != std::string::npos) {
      return (int64_t)28 * 24 * 60 * 60 * 1'000'000'000;
    } else {
      THROW() << "Not new channel: " << channel;
    }

    return std::numeric_limits<int64_t>::quiet_NaN();
  }

  static void BuildKlineRequest(
      const std::vector<std::string>& channels,
      const std::vector<std::string>& symbols,
      const std::map<std::string, std::string>& channel_templates,
      const std::string& request_template,
      int64_t timestamp,
      const WebsocketConfig* config,
      std::map<std::tuple<std::string, std::string>,
      boost::posix_time::ptime>* last_long_query_time,
      std::vector<std::string>* rest_channels,
      std::vector<std::string>* reqs) {
    std::vector<std::string> ch = channels;
    std::vector<std::string> ch_kline;
    auto iter = ch.begin();
    while (iter != ch.end()) {
      if (StringStartsWith(*iter, "kline_rest")) {
        ch_kline.push_back(*iter);
        iter = ch.erase(iter);
      } else {
        iter++;
      }
    }

    if (!ch_kline.empty()) {
      boost::posix_time::ptime round_time =
          base::feed::RequestGenerator::TimestampRoundHour(timestamp);

      for (auto& ch_k : ch_kline) {
        std::string c = channel_templates.at(ch_k);
        auto interval_time = GetPeriod(c);
        for (auto& symbol : symbols) {
          std::string length;
          bool long_query = base::feed::RequestGenerator::CheckLongQuery(
              round_time, symbol, ch_k, last_long_query_time);
          if (long_query) {
            if (!config->kline_start().empty()) {
              int64_t start_time, end_time;
              start_time = TimestampFromIso8601(config->kline_start());
              // huobi spot alway response lastest kline data
              end_time = MarketTimestampProvider::GetTimestamp();
              int64_t length_val = (end_time - start_time) / interval_time + 2;  // 2 redundancy
              if (length_val > 2000) {
                length = "2000";
                LOG(INFO) << "Huobi kline from/to too long, only supports 2000 klines from now";
              } else {
                length = std::to_string(length_val);
              }
            } else if (stoll(config->kline_length()) > 2000) {
              length = "2000";
              LOG(INFO) << "Huobi kline length too big: " << config->kline_length();
            } else {
              length = config->kline_length();
            }
          } else {
            length = "3";  // query small data
          }
          std::string c_copy = c;
          boost::replace_all(c_copy, "{}", symbol);
          boost::replace_all(c_copy, "{length}", length);
          std::string s = request_template;
          boost::replace_all(s, "{channel}", c_copy);
          reqs->emplace_back(s);
        }
      }
    }

    *rest_channels = ch;
  }

  static void GetUnsubscribeTemplate(
      const std::vector<std::string>& channels,
      const std::map<std::string, std::string>& channel_templates,
      std::vector<std::string>* channels_adjust,
      std::map<std::string, std::string>* channel_templates_adjust) {
    std::vector<std::string> channels_copy = channels;
    for (auto [channel, channel_template] : channel_templates) {
      if (channel_template.find("\"req\"") != std::string::npos) {
        // ignore "req" ops message, it's pull style
        auto pos = std::remove(channels_copy.begin(), channels_copy.end(), channel);
        channels_copy.erase(pos, channels_copy.end());
        continue;
      }

      boost::replace_all(channel_template, "\"sub\"", "\"unsub\"");
      channel_templates_adjust->insert({channel, channel_template});
    }

    *channels_adjust = channels_copy;
  }

 private:

  const WebsocketConfig* config_;
};

}  // namespace impl
using impl::HuobiRequestGenerator;
}  // namespace coin2::exchange::huobi::feed_v1
